<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,height=device-height,inital-scale=1.0,maximum-scale=1.0,user-scalable=no;" />
<style type="text/css">
    * {margin:0;padding:0; outline:0}
    body {text-align:center}
	h3 {text-align:center;line-height:3em;text-shadow:1px 1px 3px #222}
	div {margin-top:2em}
	button{height:3em;width:40%}
	button:first-child{margin-right:6%}
    textarea {width: 88%;
margin-top: 2em;
min-height: 8em;}
</style>
<h3>Audio Recorder Demo</h3>
<audio id="audio" autoplay controls></audio>
<div>
<button id="start">Start</button>
<button id="stop" disabled>Stop</button>
</div>
<textarea id="output"></textarea>
<input type="button" id="test" value="test">
<script>
	// hack
	navigator.getUserMedia = (navigator.getUserMedia || 
                    navigator.webkitGetUserMedia || 
                    navigator.mozGetUserMedia || 
                    navigator.msGetUserMedia);
    window.AudioContext = AudioContext || webkitAudioContext || mozAudioContext || msAudioContext;
    window.URL = URL || webkitURL || mozURL || msURL;
	
	function test() {
		Log(navigator.getUserMedia);
		Log(AudioContext);
		Log(URL);
		Log(ArrayBuffer);
		Log(Blob);
	}
	
	var audioStream,
		leftChl=[],
		rightChl=[],
		recLength=0,
		btnStart=$("start"),
		btnStop=$("stop"),
		output=$('output');
	window.onerror = Log;
	
	function $(sel){return document.getElementById(sel)}
	$("test").onclick = test;
	
	function Log(msg) {
		var older = output.value;
		output.value = older + "\n"+msg;
	}
	function openMic() {
		this.disabled = true;
		btnStop.removeAttribute('disabled');
		navigator.getUserMedia({audio:true}, function(stream) {
			audioStream = stream;
			Log("proc stream");
			try {
				var ac = new AudioContext,
					recorder = ac.createScriptProcessor(2048, 2, 2),
					microphone = ac.createMediaStreamSource(stream),
					filter = ac.createBiquadFilter();
				// 缓冲的处理事件
				recorder.onaudioprocess = function(e){
					var ib = e.inputBuffer;
					leftChl.push(new Float32Array(ib.getChannelData(0)));
					rightChl.push(new Float32Array(ib.getChannelData(1)));
					recLength += 2048;
				};
				microphone.connect(filter);
				filter.connect(recorder);			// 音量节点链接到缓冲
				recorder.connect(ac.destination);	// 缓冲链接到扬声器
				Log("proc stream done.");
			} catch(e) {
				Log(e.name + ":"+ (e.message||"") + " " + (e.stack||"") )
			}
		}, function(e){Log(e.name + ":"+ (e.message||"") + " " + (e.stack||"") )});
	}

	/**
	 * 合并缓冲
	 */
	function mergeBuffers(buf, len){
		var ret= new Float32Array(len),
			offset = 0,
			l = buf.length,
			i=0,b;
		while(i<l){
			b = buf[i++];
			ret.set(b, offset);
			offset += b.length;
		}
		return ret;
	}

	function interleave(left, right){
		var l= left.length + right.length,
			ret = new Float32Array(l),
			idx = 0,i=0;
		while(i<l){
			ret[i++] = left[idx];
			ret[i++] = right[idx];
			++idx;
		}
		return ret;
	}

	function writeUTFBytes(view, offset, string){ 
		var l = string.length,i=0;
		for (; i < l; ++i){
			view.setUint8(offset + i, string.charCodeAt(i));
		}
	}

	function getWAV(left, right, recLen) {
		Log("proc left and right buffer.");
			// 分别对左右缓冲扁平化处理, 然后混合在一起
		var	result = interleave(
					mergeBuffers(left, recLen),
					mergeBuffers(right, recLen)),
			rLen = result.length,
			// 为.wav文件创建个缓冲
			buffer = new ArrayBuffer(44 + result.length * 2),
			view = new DataView(buffer);

		// 参考: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
		// RIFF chunk descriptor
		writeUTFBytes(view, 0, 'RIFF');
		view.setUint32(4, 44 + rLen * 2, true);
		writeUTFBytes(view, 8, 'WAVE');
		// FMT sub-chunk
		writeUTFBytes(view, 12, 'fmt ');
		view.setUint32(16, 16, true);
		view.setUint16(20, 1, true);
		// stereo (2 channels)
		view.setUint16(22, 2, true);
		view.setUint32(24, 44100, true);
		view.setUint32(28, 44100 * 4, true);
		view.setUint16(32, 4, true);
		view.setUint16(34, 16, true);
		// data sub-chunk
		writeUTFBytes(view, 36, 'data');
		view.setUint32(40, rLen * 2, true);

		// write the PCM samples
		var idx = 44,
			vol = 1,i=0;
		for (; i<rLen; ++i){
		    view.setInt16(idx, result[i] * (0x7FFF * vol), true);
		    idx += 2;
		}
		Log("build done.");
		// 最终生成的Blob
		return new Blob ( [ view ], { type : 'audio/wav' } );
	}

	function closeMic() {
		this.disabled = true;
		btnStart.removeAttribute('disabled');
		audioStream.stop();
		var au = $("audio");
		try {
			au.src = URL.createObjectURL(getWAV(leftChl, rightChl, recLength));
			au.play();
		} catch(e) {
			Log(e.name + ":"+ (e.message||"") + " " + (e.stack||"") )
		}
		
		leftChl=[];
		rightChl=[];
		recLength=0;
	}

	btnStart.addEventListener("click", openMic, false);
	btnStop.addEventListener("click", closeMic, false);
</script>